/* Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.activiti.workflow.simple.converter.listener;

import org.activiti.bpmn.model.Process;
import org.activiti.bpmn.model.*;
import org.activiti.workflow.simple.converter.ConversionConstants;
import org.activiti.workflow.simple.converter.WorkflowDefinitionConversion;
import org.activiti.workflow.simple.converter.WorkflowDefinitionConversionFactory;
import org.activiti.workflow.simple.definition.WorkflowDefinition;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

/**
 * Default listener for {@link WorkflowDefinitionConversion} lifecycle events.
 * <p>
 * When added to a {@link WorkflowDefinitionConversionFactory}, this class will
 * make sure a start event and end event is created for the {@link Process}.
 * Further, it will generate a correct incoming and outgoing sequence flow list
 * for each {@link Process} element, as required by some toolings (eg Modeler).
 *
 * @author Joram Barrez
 */
public class DefaultWorkflowDefinitionConversionListener implements WorkflowDefinitionConversionListener {

    private static final long serialVersionUID = -145978383684020118L;

    private static final String START_EVENT_ID = "start";

    private static final String END_EVENT_ID = "end";

    public void beforeStepsConversion(WorkflowDefinitionConversion conversion) {
        initializeProcess(conversion);
    }

    protected void initializeProcess(WorkflowDefinitionConversion conversion) {
        WorkflowDefinition workflowDefinition = conversion.getWorkflowDefinition();

        // Create new process
        Process process = conversion.getProcess();
        process.setId(generateProcessId(workflowDefinition));
        process.setName(workflowDefinition.getName());
        process.setDocumentation(workflowDefinition.getDescription());

        if (workflowDefinition.getCategory() != null) {
            conversion.getBpmnModel().setTargetNamespace(workflowDefinition.getCategory());
        }

        conversion.setProcess(process);

        // Add start-event
        StartEvent startEvent = new StartEvent();
        startEvent.setId(START_EVENT_ID);

        if (workflowDefinition.getStartFormDefinition() != null && workflowDefinition.getStartFormDefinition().getFormKey() != null) {
            startEvent.setFormKey(workflowDefinition.getStartFormDefinition().getFormKey());
        }

        process.addFlowElement(startEvent);
        conversion.setLastActivityId(startEvent.getId());
    }

    /**
     * @param workflowDefinition
     * @return process definition id that is randomized, to avoid name clashes
     * (eg. amongst differnt tenants)
     */
    protected String generateProcessId(WorkflowDefinition workflowDefinition) {
        String processId = null;
        if (workflowDefinition.getId() != null) {
            processId = workflowDefinition.getId();
        } else {
            // Revert to using the name of the process
            if (workflowDefinition.getName() != null) {
                processId = workflowDefinition.getName().replace(" ", "_");
            }
        }
        return processId;
    }

    public void afterStepsConversion(WorkflowDefinitionConversion conversion) {
        // Add end-event to process
        Process process = conversion.getProcess();

        EndEvent endEvent = new EndEvent();
        endEvent.setId(END_EVENT_ID);
        process.addFlowElement(endEvent);

        // Sequence flow from last created activity to end
        SequenceFlow sequenceFlow = new SequenceFlow();
        sequenceFlow.setId(conversion.getUniqueNumberedId(ConversionConstants.DEFAULT_SEQUENCEFLOW_PREFIX));
        sequenceFlow.setSourceRef(conversion.getLastActivityId());
        sequenceFlow.setTargetRef(END_EVENT_ID);
        process.addFlowElement(sequenceFlow);

        // To make the generated workflow compatible with some tools (eg the
        // Modeler, but also others),
        // We must add the ingoing and outgoing sequence flow to each of the flow
        // nodes
        SequenceFlowMapping sequenceFlowMapping = generateSequenceflowMappings(process);

        for (FlowNode flowNode : process.findFlowElementsOfType(FlowNode.class)) {
            List<SequenceFlow> incomingSequenceFlow = sequenceFlowMapping.getIncomingSequenceFlowMapping().get(flowNode.getId());
            if (incomingSequenceFlow != null) {
                flowNode.setIncomingFlows(incomingSequenceFlow);
            }

            List<SequenceFlow> outgoingSequenceFlow = sequenceFlowMapping.getOutgoingSequenceFlowMapping().get(flowNode.getId());
            if (outgoingSequenceFlow != null) {
                flowNode.setOutgoingFlows(outgoingSequenceFlow);
            }
        }
    }

    protected SequenceFlowMapping generateSequenceflowMappings(Process process) {
        HashMap<String, List<SequenceFlow>> incomingSequenceFlowMapping = new HashMap<String, List<SequenceFlow>>();
        HashMap<String, List<SequenceFlow>> outgoingSequenceFlowMapping = new HashMap<String, List<SequenceFlow>>();

        for (FlowElement flowElement : process.findFlowElementsOfType(SequenceFlow.class)) {
            SequenceFlow sequenceFlow = (SequenceFlow) flowElement;
            String srcId = sequenceFlow.getSourceRef();
            String targetId = sequenceFlow.getTargetRef();

            if (outgoingSequenceFlowMapping.get(srcId) == null) {
                outgoingSequenceFlowMapping.put(srcId, new ArrayList<SequenceFlow>());
            }
            outgoingSequenceFlowMapping.get(srcId).add(sequenceFlow);

            if (incomingSequenceFlowMapping.get(targetId) == null) {
                incomingSequenceFlowMapping.put(targetId, new ArrayList<SequenceFlow>());
            }
            incomingSequenceFlowMapping.get(targetId).add(sequenceFlow);
        }

        SequenceFlowMapping mapping = new SequenceFlowMapping();
        mapping.setIncomingSequenceFlowMapping(incomingSequenceFlowMapping);
        mapping.setOutgoingSequenceFlowMapping(outgoingSequenceFlowMapping);
        return mapping;
    }

    static class SequenceFlowMapping {

        protected HashMap<String, List<SequenceFlow>> incomingSequenceFlowMapping;
        protected HashMap<String, List<SequenceFlow>> outgoingSequenceFlowMapping;

        public HashMap<String, List<SequenceFlow>> getIncomingSequenceFlowMapping() {
            return incomingSequenceFlowMapping;
        }

        public void setIncomingSequenceFlowMapping(HashMap<String, List<SequenceFlow>> incomingSequenceFlowMapping) {
            if (incomingSequenceFlowMapping != null) {
                this.incomingSequenceFlowMapping = incomingSequenceFlowMapping;
            } else {
                this.incomingSequenceFlowMapping = new HashMap<String, List<SequenceFlow>>();
            }
        }

        public HashMap<String, List<SequenceFlow>> getOutgoingSequenceFlowMapping() {
            return outgoingSequenceFlowMapping;
        }

        public void setOutgoingSequenceFlowMapping(HashMap<String, List<SequenceFlow>> outgoingSequenceFlowMapping) {
            if (outgoingSequenceFlowMapping != null) {
                this.outgoingSequenceFlowMapping = outgoingSequenceFlowMapping;
            } else {
                this.outgoingSequenceFlowMapping = new HashMap<String, List<SequenceFlow>>();
            }
        }

    }

}
